passport를 사용한 로그인과 세션
✒️ 2025-05-26 14:07 내용 수정
Node.js 교과서 개정 3판 내용 정리
passport를 사용한 로그인과 세션
-
passport 공식 사이트 : https://www.passportjs.org/
- passport를 사용한 로그인 인증 수단(구글, 카카오, 트위터 등)은 아래 링크를 참조.
- https://www.passportjs.org/packages/
-
express-session, passport, passport-local, bcrypt 사용하여 로그인과 세션 기능을 구현한다.
- express 프레임워크와 세션 관리 및 저장을 위한 express-session(세션), express-mysql-session(세션 DB 저장)
- 세션은 기본으로 메모리에 저장되기 때문에 세션을 저장하려면 DB에 저장해야 한다.
- express-session 참고.
- 세션은 기본으로 메모리에 저장되기 때문에 세션을 저장하려면 DB에 저장해야 한다.
- 로그인 기능을 위한 passport, passport-local(로컬 로그인 구현)
- 암호화를 위한 bcrpyt
- express 프레임워크와 세션 관리 및 저장을 위한 express-session(세션), express-mysql-session(세션 DB 저장)
-
DB에서 사용자 데이터를 가져오고 조회하기 때문에 Sequelize 설정이 되어 있어야 한다.
로그인 설정
- 로그인 진행 과정
- /login 라우터를 통해 로그인 요청이 들어온다.
- 라우터 내에서 passport.authenticate() 메소드를 호출한다.
- 로그인 전략(LocalStrategy)를 수행한다.
- 로그인이 성공하면 사용자의 정보를 담은 객체와 함께 req.login() 을 호출한다.
- req.login 메소드가 passport.serializeUser()를 호출한다.
- req.session에 사용자의 아이디만 저장한 세션을 생성하고, express-session에 설정한 방법대로 브라우저 connect.sid 세션 쿠키를 전송한다.
- 로그인이 완료된다.
- 로그인 이후 진행 과정
- 요청이 들어오면 라우터에 요청이 도달하기 전 passport.session 미들웨어가 passport.deserializeUser()를 호출한다.
- connect.sid 세션 쿠기를 읽고 세션 객체를 찾아 req.session으로 만든다.
- req.session에 저장된 아이디를 사용하여 DB에 사용자 정보를 조회하고, 조회된 정보를 req.user에 저장한다.
- 먼저 필요한 라이브러리를 VSC 터미널로 설치한다.
npm i express express-session express-mysql-session passport passport-local bcrypt
- server.js에서 필요한 라이브러리를 모두 importgkrh, express 설정을 먼저 진행한다.
// 1. 모듈 - require
const express = require('express');
const app = express();
const session = require('express-session');
const passport = require('passport');
const LocalStrategy = require('passport-local');
// 2. use, set - 등록
app.set('view engine', 'ejs');
app.use(express.static(__dirname + '/public'));
app.use(express.json());
app.use(express.urlencoded({extended : true}));
- sequelize를 통한 db 설정 및 객체를 가져온다.
// db
const db = require('./models');
const { User } = db;
- 세션을 저장할 DB 설정을 진행한다.
const MySQLStore = require("express-mysql-session")(session);
// MySQLStore 옵션
// 세션을 저장할 DB 옵션을 설정
const dbOption = {
host: '127.0.0.1',
port: '3306',
user: 'root',
password: 'password',
database: 'databaseName',
}
- passport 설정을 진행한다.
passport.initialize(),app.use(session()),app.use(passport.session())메소드 순서는 반드시 지켜야 한다.passport.serializeUser(): 로그인 시 실행되며,req.session()객체에 저장할 데이터를 결정한다.passport.deserializeUser(): 매 요청마다 실행되며,passport.serializeUser()를 통해 저장된req.session()객체 내에 아이디 혹은 이메일 등의 정보를 가져와 DB에서 사용자의 정보를 조회하고, 조회한 정보를req.user에 저장한다.deserializeUser()를 통해 세션에 불필요한 정보를 담아두지 않아도 되며, 로그인 후엔 사용자의 정보를req.user에서 가져와서 사용할 수 있다.
// 세션을 사용하기 위한 옵션 설정
app.use(session({
secret: '4154@#$%&*(6586/*163',
resave : false,
saveUninitialized : false,
cookie : {maxAge : 60 * 60 * 1000},
store : new MySQLStore( dbOption ), // 세션 저장위치
}));
// 초기화, 사용자 인증 요청 처리할수 있도록 함
// app.use(session)보다 뒤에 와야 함
app.use(passport.initialize());
// 세션 사용하도록 설정
app.use(passport.session());
// 로그인 검증 - 검증 방법
passport.use(new LocalStrategy(async (username, pw, done) => {
// 사용자 정보가 DB에 존재하는지 조회
let result = await User.findOne({ where : { username : username}});
if (!result) { // 사용자 정보가 없을 시
return done(null, false, { message: '아이디 DB에 없음' });
}
// 비밀번호를 암호화 후 비교
let validPw = await bcrypt.compare(pw, result.password)
if (!validPw) {
return done(null, false, { message: '비밀번호 불일치' });
}
// DB에 정보가 있고 비밀번호도 일치한다면 통과
return done(null, result);
}));
// 로그인 시 실행
// req.session 객체에 데이터를 저장해준다.
passport.serializeUser( (user, done) =>{
process.nextTick(() => {
// 첫 번째는 에러, 두 번째는 저장할 데이터를 전달
// 사용자의 id와 이름만 저장
done(null, { id: user.id, username: user.username });
});
});
// 매 요청 때마다 실행
// serializeUser에서 저장한 데이터가 deserializeUser의 매개변수
// serializeUser()에서 사용자 정보 객체의 id만 저장하고,
// deserializeUser에서 해당 id로 사용자 정보 객체를 불러온다.
passport.deserializeUser( async (user, done) => {
// 사용자의 정보를 조회
let result = await User.findOne({where : {id : user.id}})
if(result) { // 정보가 있다면 객체에 정보 저장
const newUserInfo={
id : result.id,
username : result.username
}
process.nextTick(() => {
return done(null, newUserInfo); // req.user에 newUserInfo를 저장
});
}
});
- 로그인과 연관된 라우터를 설정한다.
- login 라우터에서 passport.authenticate()를 호출하면 LocalStrategy를 수행하고, 검증이 완료되면 req.login을 호출해 로그인을 진행한다.
// 3. listen - 포트번호 지정
let port = 8081;
app.listen(port , ()=>{
console.log('접속 성공! - http://localhost:'+port)
});
// 4. 라우팅
app.get('/', async (req, res)=>{
res.redirect('/login');
});
// 로그인 페이지로 이동
app.get('/login', (req, res)=>{
res.render('login.ejs');
});
// 로그인 실패 시 처리
app.get('/login/:state', (req, res)=>{
let {state} = req.params
if(state== 'fail'){
res.render('login-fail.ejs');
}
});
// 로그인
app.post('/login', (req, res)=>{
// passport.authenticate()가 호출되면 LocalStrategy를 수행
passport.authenticate('local', (error, user, info)=>{ // 만약 외부 api로 로그인 한다면 'local'을 변경
// DB 에러
if (error) return res.status(500).json(error);
// DB에 등록된 계정이 없음
if (!user) return res.redirect('/login/fail');
// 로그인
req.logIn(user, (err) => {
if (err) return next(err);
res.redirect('/main');
})
})(req,res);
});
// 메인 : 로그인 후 메인 페이지
app.get('/main', async (req, res) => {
if (req.isAuthenticated()) { // 사용자가 로그인 한 상태인지 확인하는 메소드
const userInfo = req.user; // req.user에서 사용자 정보를 가져올 수 있다.
const username = userInfo.username;
const userId = userInfo.id;
try {
res.render('main.ejs');
} catch (error) {
console.log(error);
res.status(500).send('서버 DB 연결 에러!');
}
} else {
res.redirect("/login");
}
});
회원 가입
- 회원가입은 DB에 해당 사용자의 정보가 이미 존재하는지 조회하고, 존재한다면 isJoined라는 변수에 정보를 담아 페이지에 출력하여 회원가입 실패를 사용자에게 알려준다.
- 해당 정보를 가진 사용자가 없다면 DB에 새 사용자를 추가하며, 비밀번호는 bcrypt를 이용해 암호화하여 저장한다.
// 회원가입
app.get("/join", async function(req, res) {
res.render("join.ejs", {isJoined : {message : ''}});
})
app.post("/join", async function(req, res) {
const newUser = { // 회원가입 요청 정보
username : req.body.username,
password : await bcrypt.hash(req.body.password, 12)
}
try {
// DB에서 해당 사용자가 존재하는지 조회
const result = await User.findOne({where: {username : newUser.username}});
let isJoined = '';
if (result == null) {
// 비밀번호 암호화
const hashPwd = ;
await User.create(newUser); // 새 회원 등록
isJoined = 'success';
// 회원가입 완료 메시지를 전송
res.render("join.ejs", {isJoined : {message : isJoined}});
} else {
isJoined = 'fail';
// 회원가입 실패 메시지를 전송
res.render("join.ejs", {isJoined : {message : isJoined}});
}
} catch (error) {
res.redirect("/error");
}
});
로그아웃
- req.logout()은 req.user 객체와 req.session 객체를 제거하고, DB에 저장된 session의 사용자 정보도 자동으로 제거한다.
- 사용자 정보가 제거된 DB 데이터는
express-mysql-session기본 설정에 따라 만료일이 지난 경우엔 일정 기간 후 자동으로 제거된다. - 참고 자료 : npm express-mysql-session
- 사용자 정보가 제거된 DB 데이터는
// 로그아웃
app.get('/logout', (req, res)=>{
req.logout(()=>{
res.redirect('/login');
});
});